/**
 * Copyright (c) 2017 MomaThink (Beijing) Technology Inc. All rights reserved.
 * Company:摩码创想（北京）科技有限公司
 *
 */
package com.rjj.wlqq.importExcel;

import com.jfinal.kit.PathKit;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;
import com.rjj.wlqq.tool.excel.ry_excel.StringUtils;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Font;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * XLS导入工具
 *
 * @author dufuzhong
 * @date 2017年8月9日 上午10:14:01
 */
public abstract class ImportXls {

    private final Logger log = LoggerFactory.getLogger(ImportXls.class);
    protected Map<String, HashSet<String>> checkContains = new ConcurrentHashMap<>();
    protected StringBuilder msgs = new StringBuilder();
    protected long isOK = 0;
    protected long isNo = 0;
    public String errorExcelPath = null;//错误excel地址

    /**
     * K 必须和xls列头一样 ,    V 必须和数据库一样 <br>
     * 例子: return new Kv().set("姓名","name") ...
     * 把excel中的汉字和数据库的字段对饮关系建立
     *
     * @param index sheet的页数
     */
    protected abstract Map<Object, Object> getTitleAndColumn(int index);


    /**
     * 检查  record 是否符合数据库写入要求(字段已处理两端空格和空的情况都=null)
     *
     * @param rowIndexCount : rowIndex + 1
     * @param tempcell
     * @return 允许写入:返回true        不允许写入:返回false
     */
    protected boolean isCheckRecord(Record record, int rowIndex, int rowIndexCount, HSSFCell tempcell, String key) {
        return true;
    }

    /**
     * 检查  record 的字段 是否符合数据库字段写入要求(字段已处理两端空格和空的情况都=null)
     * <p>
     * 可以重写，做一些格式化数据，比如时间，根据姓名拿到id等
     * <p>
     * 【column（parentName），value(小王)】
     *
     * @param record        所有的数据都在这里
     * @param column        key
     * @param value         值
     * @param rowIndex      行下标
     * @param rowIndexCount 第几行
     */
    protected Record setRecord(Record record, String column, Object value, int rowIndex, int rowIndexCount) {
        return record.set(column, value);
    }

    /**
     * 检查  record 是否 需要 停止读取
     *
     * @param rowIndexCount : rowIndex + 1
     * @return 停止读取:返回true        不停止读取:返回false
     */
    private boolean isCheckBreak(Record record, int rowIndex, int rowIndexCount) {
        return false;
    }

    /**
     * 单表导入
     *
     * @param file:      XLS 文件
     * @param tableName: 表名，批量插入数据时候用，这里注释了
     * @return Kv: isOK=成功数 , isNo=失败数 , length=执行总数 , errcode=异常状态 , errmsg=异常信息
     */
    public Map<String, Object> save(MultipartFile file, String tableName, Map<String, Object> records, Progress progress, String key) {

        //导入数据处理情况
        Map<String, Object> ret = new HashMap<>();
        ret.put("isOK", 0);
        ret.put("isNo", 0);
        ret.put("length", 0);
        ret.put("errcode", 0);

        try {
            //获取excel表格
            HSSFWorkbook hwb = new HSSFWorkbook(file.getInputStream());

            //预留多sheet导入，导入第一个sheet页的数据
            int index = 0;

            HSSFSheet sheet = hwb.getSheetAt(index);

            Map<Object, String> columns = new HashMap<>();

            //把数据封装到这个里边，可以实现好几条数据一块保存，一次保存多条记录
            //List<Record> recordList = new ArrayList<Record>();
            //Map<Object, String> recordRowIndex = new HashMap<>();//记录record角标的

            //获取的是表头的分装数据
            /*
             *  0  parentName
             *  1  xxx
             *  2  aaa
             *
             *  返回值，两个 -1  Excel中出现多余字段
             *               0  数据正确。字段正好
             */
            int rowIndex = getColumns(sheet, columns, index, records) + 1;

            // 如果==0  就是 Excel中出现多余字段，不做导入操作
            if (rowIndex == 0) {
                //hwb.close();
                ret.put("errcode", "请使用提供的模板！");
                return ret;
            }

            //行循环
            boolean doValve = true;

            HSSFCellStyle cellStyle = setErrorTextColor(hwb);

            do {
                //从第二行数据开始，第一行是标题行
                HSSFRow row = sheet.getRow(rowIndex);
                if (row != null) {
                    //行号 下标为1，但是是第二行的数据
                    int rowIndexCount = rowIndex + 1;

                    Record record = new Record();

                    HSSFCell tempcell = createErrorCell(cellStyle, row);

                    //循环列，列的长度就是表头行的长度
                    for (int cellnum = 0; cellnum < columns.size(); cellnum++) {

                        // 表头行数据是【0   parantName】,key就是数字，这样就可以拿到对应数据库字段 parantName
                        String column = columns.get(cellnum);

                        //这个是读取的excel表中的第一列的数据
                        HSSFCell cell = row.getCell(cellnum);

                        //格式化数据
                        if (cell != null) {
                            String value = getCellValue(cell).toString();
                            if (StringUtils.isNotBlank(value)) {
                                //把数据封装到record中【parentName,小王】    //把column, value,放到record中
                                setRecord(record, column, value, rowIndex, rowIndexCount);
                            }
                        }
                    }

                    /*
                     * 这里检查数据的正确性，不符合我的规则的都可以在这里处理，格式化时间，判断重复，保存
                     * 这里可以一行一行的保存
                     */
                    boolean checkRecord = isCheckRecord(record, rowIndex, rowIndexCount, tempcell, key);

                    /*
                     * 可以把代码注释的放开，可以做批量处理
                     * checkRecord数据正确，下边就把数据放到集合中
                     */
                    if (checkRecord) {
                        // recordList.add(record);
                        // recordRowIndex.set(record, rowIndex);
                    } else {
                        //isNo ++;
                    }

                    //设置进度条
                    setProgress(key, progress, sheet, rowIndexCount);

                    //可以重写，遇到什么情况下，就停止读取数据了
                    if (isCheckBreak(record, rowIndex, rowIndexCount)) {//不停止
                        break;
                    }
                    rowIndex++;
                    continue;//下一轮遍历
                }
                doValve = false;
            } while (doValve);


            //这里可以做批量处理，tableName就是我们要保存到数据库中表的名字
           /* int[] batchSave = batchSave(tableName, recordList);
            for (int i = 0; i < batchSave.length; i++) {
                if(batchSave[i] == 1){
                    isOK ++;
                }else{
                    isNo ++;
                    appendMsg(recordRowIndex.getInt(recordList.get(i)) + 1 , "全部", "数据写入失败,请检查格式");
                }
            }*/

            //导入数据中有的错误信息保存到excel中写到服务器中，然后提供用户下载查看
            String errorExcelPath = writeErrorExcel(records, hwb);

            ret.put("isOK", isOK);
            ret.put("isNo", isNo);
            ret.put("length", isOK + isNo);
            //hwb.close();
        } catch (Exception e) {
            e.printStackTrace();
            ret.put("errcode", 500);
            ret.put("errmsg", e.toString());
        }

        records.put("successnumber", isOK);
        records.put("failnumber", isNo);
        //导入完成状态
        records.put("state", "1");
        return ret;
    }

    /**
     * 创建错误信息单元格，并写上错误的信息
     */
    private HSSFCell createErrorCell(HSSFCellStyle cellStyle, HSSFRow row) {
        //提示错误行的信息
        HSSFCell tempcell = row.createCell(row.getLastCellNum() + 1);//最后一列
        tempcell.setCellType(CellType.STRING);
        tempcell.setCellStyle(cellStyle);//设置单元格样式
        return tempcell;
    }

    private HSSFCellStyle setErrorTextColor(HSSFWorkbook hwb) {
        //错误行提示的文字颜色
        HSSFCellStyle cellStyle = hwb.createCellStyle();//创建单元个样式
        Font font = hwb.createFont();
        font.setFontHeightInPoints((short) 16);
        font.setFontName("楷体");
        //font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
        font.setBold(true);
        font.setColor(Font.COLOR_RED);
        cellStyle.setFont(font);
        return cellStyle;
    }

    /**
     * 输出错误的excel
     * <p>
     * 有错误信息的时候生成excel表格，提供下载查错
     *
     * @param records 记录信息
     * @param hwb     表格
     */
    private String writeErrorExcel(Map<String, Object> records, HSSFWorkbook hwb) {
        if (isNo == 0) {
            return null;
        }
        String outputFileName = System.currentTimeMillis() + ".xls";//错误excel名称
        String downLoadPath = "/download/errorExcel/" + outputFileName;//错误excel地址
        FileOutputStream stream;

        try {
            records.put("downloadurl", downLoadPath);
            records.put("remark", "请下载查看");
            File filePath = new File(PathKit.getWebRootPath() + downLoadPath);
            if (!filePath.getParentFile().exists()) {
                filePath.getParentFile().mkdirs();
            }
            stream = new FileOutputStream(filePath);
            hwb.write(stream);
            stream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return downLoadPath;
    }


    /**
     * 设置进度条
     *
     * @param key           唯一性的标示符
     * @param progress      进度条实例
     * @param sheet         sheet
     * @param rowIndexCount 行号
     */
    private void setProgress(String key, Progress progress, HSSFSheet sheet, int rowIndexCount) {
        //****************************************************
        //进度条设置
        // Progress progress = new Progress();
        //第几行的数据
        progress.set(Progress.PROGRESS_ + key, new Record().set("row", rowIndexCount - 1).set("isOk", isOK).set("isNo", isNo).set("total", sheet.getPhysicalNumberOfRows()));
        log.info("进度条设置进度数据：第" + (rowIndexCount - 1) + "行数据开始导入");
        //****************************************************
    }

    /**
     * 批量写入数据库
     */
    protected int[] batchSave(String tableName, List<Record> recordList) {
        return Db.batchSave(tableName, recordList, 200);
    }

    /**
     * 从XLS表格中取值，转化为字符串
     */
    private Object getCellValue(Cell cell) {
        Object val = "";
        if (StringUtils.isNotNull(cell)) {
            if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) {
                val = cell.getNumericCellValue();
                if (DateUtil.isCellDateFormatted(cell)) {
                    val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
                } else {
                    if ((Double) val % 1 > 0) {
                        val = new BigDecimal(val.toString());
                    } else {
                        val = new DecimalFormat("0").format(val);
                    }
                }
            } else if (cell.getCellType() == CellType.STRING) {
                val = cell.getStringCellValue();
            } else if (cell.getCellType() == CellType.BOOLEAN) {
                val = cell.getBooleanCellValue();
            } else if (cell.getCellType() == CellType.ERROR) {
                val = cell.getErrorCellValue();
            }
        }
        return val;
    }

    /**
     * 获取XLS标题开始的行数位置
     */
    protected int getColumnsRowIndex(HSSFSheet sheet, Map<Object, String> columns, int index) {
        return 0;
    }

    /**
     * 获取XLS与数据库对相应关系键值,
     * <p>
     * 封装excel表头
     *
     * @param sheet   excel的sheet页，可以拿到数据
     * @param columns 把数据以k-v形式封装到map中
     * @param index   sheet的下标
     * @param map     存放处理的信息，
     *                <p>
     *                返回值两个，  -1  请使用模板，excel表中存在多余字段
     *                0 数据正确，excel表格中没有多余字段
     */
    @SuppressWarnings("deprecation")
    protected int getColumns(HSSFSheet sheet, Map<Object, String> columns, int index, Map<String, Object> map) {

        //默认第一行 下标为0
        int rowIndex = getColumnsRowIndex(sheet, columns, index);

        //获得excel 标题行
        HSSFRow titleRow = sheet.getRow(rowIndex);

        // 这里主要是把封装的excel中的字段和数据库的字段对应的关系拿到  【家长姓名<-----> parentName】
        Map<Object, Object> t = getTitleAndColumn(index);//获得标题行的自己封装的kv


        //循环遍历列，从第一列开始，把一行数据封装到map中
        int cellnum = 0;
        boolean doValve = true;
        do {

            //获取第一个单元格
            HSSFCell cell = titleRow.getCell(cellnum);//标题行的第一个cell

            if (cell != null) {

                //值设置为字符串类型
                cell.setCellType(CellType.STRING);

                //取出值，这个值就是excel表格中的汉字
                String key = cell.getStringCellValue();


                if (key != null && !"".equals(key)) {

                    //这里根据 【家长姓名<-----> parentName】关系，key为excel表格的汉字，值是我们封装写的数据库字段，
                    /*
                     * 我们先提供一个excel模板，其中写好表头
                     *  名称 年龄 性别
                     *
                     *  然后我们在我们的方法getTitleAndColumn中封装汉字和数据库字段对应的关系
                     *
                     *  然后根据excel表格中读取的汉字，拿到对应我们封装的getTitleAndColumn中的数据库字段。
                     *  如果没有拿到，那就是excel表中多写了字段，属于无用字段，我们这里可以不管他，也可以不让他导入，
                     *
                     *  目前这里就是不让导入
                     */
                    //key == 家长姓名,  str == parentName
                    Object obj = t.get(key.trim());
                    if (null == obj) {
                        log.info("空值了，模版字段不正确，请使用提供的模板。");
                        map.put("remark", "请使用提供的模板");
                        map.put("state", "2");
                        return -1;
                    }
                    String val = obj.toString();

                    //  String str = t.get(key.trim()).toString()把数据库的对应字段添加到columns中，key = 下标(0)  值是 parentName
                    columns.put(cellnum, val);//key0123，值abc,这个把我自己封装的值放到这个kv中了

                    cellnum = cellnum + 1;
                    continue;//下一轮遍历
                }
            }
            doValve = false;
        } while (doValve);

        return rowIndex;
    }


    /**
     * 检查值XLS中 数值的唯一性, 如果是唯一的:把值存入并返回false
     */
    protected boolean isCheckContainsOrAdd(String key, String val) {
        if (val == null || key == null) {
            return false;
        }
        if (isCheckContains(key, val)) {
            return true;
        }
        addCheckContains(key, val);
        return false;
    }

    /**
     * 检查值XLS中 数值的唯一性
     */
    protected boolean isCheckContains(String key, String val) {
        if (val == null || key == null) {
            return false;
        }
        HashSet<String> hashSet = checkContains.get(key);
        if (hashSet == null) {
            return false;
        }
        return hashSet.contains(val);
    }

    /**
     * 把值存入唯一性检查集合
     */
    protected ImportXls addCheckContains(String key, String val) {
        if (val == null || key == null) {
            return this;
        }
        HashSet<String> hashSet = checkContains.get(key);
        if (hashSet == null) {
            hashSet = new HashSet<>();
            checkContains.put(key, hashSet);
        }
        hashSet.add(val);
        return this;
    }

    /**
     * 写日志
     */
    protected StringBuilder appendMsg(Object rowIndexCount, String title, String msg) {
        return msgs.append("第").append(rowIndexCount).append("行的[").append(title).append("]列 ").append(msg).append("<br>");
    }

    /**
     * 获取日志
     */
    public StringBuilder getMsgs() {
        return msgs;
    }

    /**
     * 验证时间字符串格式 是否正确 , 如果不正确返回 默认值
     */
    protected static String getCheckDateTimeStr(Object timeStr, String defaultTimeStr) {
        return isCheckDateTimeStr((String) timeStr) ? (String) timeStr : defaultTimeStr;
    }

    /**
     * 验证时间字符串格式 是否正确
     */
    protected static boolean isCheckDateTimeStr(String timeStr) {
        return timeStr.matches(
                "((19|20)[0-9]{2})-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01]) ([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]");
    }

    /**
     * 验证时间字符串格式 是否正确 , 如果不正确返回 默认值
     */
    protected static String getCheckDateStr(Object timeStr, String defaultTimeStr) {
        return isCheckDateStr((String) timeStr) ? (String) timeStr : defaultTimeStr;
    }

    /**
     * 验证时间字符串格式 是否正确
     */
    protected static boolean isCheckDateStr(String timeStr) {
        return timeStr.matches(
                "((19|20)[0-9]{2})-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])");
    }

}